package com.ihr360.hibernate.type.encrypt.internal;


import com.ihr360.encryption.KeyProperty;
import com.ihr360.encryption.MysqlCipherInitializer;
import com.ihr360.hibernate.type.json.internal.JsonTypeDescriptor;
import com.ihr360.hibernate.type.util.ObjectMapperWrapper;
import com.ihr360.hibernate.type.util.StringUtils;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

/**
 * @author David.Wei
 */
public class JsonEncryptTypeDescriptor
        extends JsonTypeDescriptor {
    final Logger logger = LoggerFactory.getLogger(getClass().getName());
    private MysqlCipherInitializer cipherInitializer;

    public JsonEncryptTypeDescriptor() {
        this.cipherInitializer = new MysqlCipherInitializer();
    }

    public JsonEncryptTypeDescriptor(ObjectMapperWrapper objectMapperWrapper) {
        super(objectMapperWrapper);
        this.cipherInitializer = new MysqlCipherInitializer();
    }

    public JsonEncryptTypeDescriptor(ObjectMapperWrapper objectMapperWrapper, Type type) {
        super(objectMapperWrapper, type);
        this.cipherInitializer = new MysqlCipherInitializer();
    }

    @Override
    public String toString(Object value) {
        String json = objectMapperWrapper.toString(value);
        if (StringUtils.isNotEmpty(json) && KeyProperty.isNotDefault(KeyProperty.DATABASE_ENCRYPTION_KEY)) {
            try {
                Cipher cipher = cipherInitializer.prepareAndInitCipher(Cipher.ENCRYPT_MODE, KeyProperty.DATABASE_ENCRYPTION_KEY);
                return encrypt(cipher, json);
            } catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException | UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }
        return json;
    }

    @Override
    public Object fromString(String encryptedStr) {
        if (StringUtils.isNotEmpty(encryptedStr) && KeyProperty.isNotDefault(KeyProperty.DATABASE_ENCRYPTION_KEY)) {
            try {
                Cipher cipher = cipherInitializer.prepareAndInitCipher(Cipher.DECRYPT_MODE, KeyProperty.DATABASE_ENCRYPTION_KEY);
                String json = decrypt(cipher, encryptedStr);
                return objectMapperWrapper.fromString(json, type);
            } catch (NoSuchAlgorithmException
                    | InvalidKeyException
                    | InvalidAlgorithmParameterException
                    | BadPaddingException
                    | NoSuchPaddingException
                    | IllegalBlockSizeException
                    | DecoderException e) {
                throw new RuntimeException(e);
            }
        }
        return objectMapperWrapper.fromString(encryptedStr, type);
    }

    private String encrypt(Cipher cipher, String json) throws IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
        byte[] bytesToEncrypt = json.getBytes("UTF-8");
        byte[] encryptedBytes = callCipherDoFinal(cipher, bytesToEncrypt);
        String encryptedStr = new String(Hex.encodeHex(encryptedBytes));
        if (logger.isDebugEnabled()){
            logger.info(String.format("Select aes_decrypt(unhex('%s'), 'pass');", encryptedStr));
        }
        return encryptedStr;
    }

    private String decrypt(Cipher cipher, String dbData) throws IllegalBlockSizeException, BadPaddingException, DecoderException {
        byte[] encryptedBytes = Hex.decodeHex(dbData.toCharArray());
        byte[] decryptedBytes = callCipherDoFinal(cipher, encryptedBytes);
        String decryptedStr = new String(decryptedBytes);
        if (logger.isDebugEnabled()){
            logger.debug(String.format("descypted str:", decryptedStr));
        }
        return decryptedStr;
    }

    byte[] callCipherDoFinal(Cipher cipher, byte[] bytes) throws IllegalBlockSizeException, BadPaddingException {
        return cipher.doFinal(bytes);
    }


}
